﻿/*
 * File: LoadingScene.cs
 * Author: Gourav Das
 * Purpose: Loading Scene
 * Created: April 30 2011
 * Major Modification: May 31 2011 - Added background thread to facilitate animation
 * Copyright: Gourav Das, 2011
 */

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using System.Diagnostics;
using System.Threading;
using _22yards._2D;
using _22yards.Scenes;

namespace _22yards.Scenes.Helpers
{
    /// <summary>
    /// The loading scene facilitates transitions between the menu system and the game itself. For larger transitions that can
    /// take a longer time to load their data, we want the menu system to be entirely
    /// gone before we start loading the game. This is done as follows:
    /// 
    /// - Tell all the existing scenes to transition off.
    /// - Activate a loading screen, which will transition on at the same time.
    /// - The loading screen watches the state of the previous screens.
    /// - When it sees they have finished transitioning off, it activates the real
    ///   next screen, which may take a long time to load its data. The loading
    ///   screen will be the only thing displayed while this load is taking place.
    /// </summary>
    class LoadingScene : Scene
    {
        bool loadingIsSlow;
        bool otherScreensAreGone;

        Scene[] scenesToLoad;

        Thread backgroundThread;
        EventWaitHandle backgroundThreadExit;

        GraphicsDevice graphicsDevice;

        GameTime loadStartTime;
        TimeSpan loadAnimationTimer;
        SpriteBatch sb;

        /// <summary>
        /// The constructor is private: loading scenes should
        /// be activated via the static Load method instead.
        /// </summary>
        private LoadingScene(SceneEngine _sceneEngine, bool _loadingIsSlow,
                              Scene[] _scenesToLoad)
        {
            this.loadingIsSlow = _loadingIsSlow;
            this.scenesToLoad = _scenesToLoad;

            TransitioningTime = TimeSpan.FromSeconds(0.5);

            if (loadingIsSlow)
            {
                backgroundThread = new Thread(BackgroundWorkerThread);
                backgroundThreadExit = new ManualResetEvent(false);

                graphicsDevice = _sceneEngine.GraphicsDevice;
                //Using a new spritebatch, because it may happen that in an draw cycle, the draw is within the spritebatch.Begin() call, when
                //the loading ends and SetModelEffTextureNNormal() is called which starts a spritebatch.Begin() again. Hence when we use a
                //different spriteBatch than the sceneengine's then we would not face problem
                sb = new SpriteBatch(graphicsDevice);
            }
        }


        /// <summary>
        /// Activates the loading screen.
        /// </summary>
        public static void Load(SceneEngine _sceneEngine, bool _loadingIsSlow,
                                PlayerIndex? _controllingPlayer,
                                params Scene[] _scenesToLoad)
        {
            // Tell all the current screens to transition off.
            foreach (Scene scene in _sceneEngine.GetScenes())
                scene.ExitScene();

            // Create and activate the loading screen.
            LoadingScene loadingScene = new LoadingScene(_sceneEngine,
                                                            _loadingIsSlow,
                                                            _scenesToLoad);

            _sceneEngine.AddScene(loadingScene, _controllingPlayer);
        }


        /// <summary>
        /// Updates the loading screen.
        /// </summary>
        public override void Update(GameTime _gameTime, bool _otherScreenHasFocus,
                                                       bool _coveredByOtherScreen)
        {
            base.Update(_gameTime, _otherScreenHasFocus, _coveredByOtherScreen);

            // If all the previous screens have finished transitioning
            // off, it is time to actually perform the load.
            if (otherScreensAreGone)
            {
                // Start up the background thread, which will update the network
                // session and draw the animation while we are loading.
                if (backgroundThread != null)
                {
                    loadStartTime = _gameTime;
                    backgroundThread.Start();
                }

                sceneEngine.RemoveScene(this);

                foreach (Scene scene in scenesToLoad)
                {
                    if (scene != null)
                    {
                        sceneEngine.AddScene(scene, ControllingPlayer);
                    }
                }


                // Signal the background thread to exit, then wait for it to do so.
                if (backgroundThread != null)
                {
                    backgroundThread.Abort();
                    backgroundThreadExit.Set();
                    backgroundThread.Join();
                }

                // Once the load has finished, we use ResetElapsedTime to tell
                // the  game timing mechanism not try to catch up.
                sceneEngine.Game.ResetElapsedTime();
            }
        }


        /// <summary>
        /// Draws the loading screen.
        /// </summary>
        public override void Draw(GameTime _gameTime)
        {
            // We check if this is the only active screen, that means all the previous screens
            // must have finished transitioning. It isn't enough just for the
            // screens to be gone: in order for the transition to look good we must
            // have actually drawn a frame without them before we perform the load.
            if ((SceneState == enumSceneState.IsActive) &&
                (sceneEngine.GetScenes().Length == 1))
            {
                otherScreensAreGone = true;
            }

            // The gameplay screen may take a while to load, so we display a loading
            // message while that is going on, but for scenes that load very quickly we don't have to display this. This 
            // bool tells us whether to draw the message or not.
            if (loadingIsSlow)
            {
                string message = "Loading";

                // Center the text in the viewport.
                Viewport viewport = sceneEngine.GraphicsDevice.Viewport;
                Vector2 viewportSize = new Vector2(viewport.Width, viewport.Height);
                Vector2 textSize = sceneEngine.TxtNFont.spFontPescadero.MeasureString(message);
                Vector2 textPosition = (viewportSize - textSize) / 2;

                Color color = Color.White * TransitionAlpha;

                // Animate the number of dots after our "Loading..." message.
                loadAnimationTimer += _gameTime.ElapsedGameTime;

                int dotCount = (int)(loadAnimationTimer.TotalSeconds * 5) % 10;

                message += new string('.', dotCount);

                // Draw the text.
                sb.Begin();
                sb.DrawString(sceneEngine.TxtNFont.spFontMotorwerk, message, textPosition, color);
                sb.End();
            }
        }

        /// <summary>
        /// Worker thread draws the loading animation and updates the network
        /// session while the load is taking place.
        /// </summary>
        void BackgroundWorkerThread()
        {
            long lastTime = Stopwatch.GetTimestamp();

            // EventWaitHandle.WaitOne will return true if the exit signal has
            // been triggered, or false if the timeout has expired. We use the
            // timeout to update at regular intervals, then break out of the
            // loop when we are signalled to exit.
            while (!backgroundThreadExit.WaitOne(1000 / 30))
            {
                GameTime gameTime = GetGameTime(ref lastTime);

                DrawLoadAnimation(gameTime);
            }
        }


        /// <summary>
        /// Works out how long it has been since the last background thread update.
        /// </summary>
        GameTime GetGameTime(ref long lastTime)
        {
            long currentTime = Stopwatch.GetTimestamp();
            long elapsedTicks = currentTime - lastTime;
            lastTime = currentTime;

            TimeSpan elapsedTime = TimeSpan.FromTicks(elapsedTicks *
                                                      TimeSpan.TicksPerSecond /
                                                      Stopwatch.Frequency);

            return new GameTime(loadStartTime.TotalGameTime + elapsedTime, elapsedTime);
        }


        /// <summary>
        /// Calls directly into our Draw method from the background worker thread,
        /// so as to update the load animation in parallel with the actual loading.
        /// </summary>
        void DrawLoadAnimation(GameTime gameTime)
        {
            if ((graphicsDevice == null) || graphicsDevice.IsDisposed)
                return;

            try
            {
                // Draw the loading screen.
                Draw(gameTime);

                graphicsDevice.Present();
            }
            catch
            {
                // If anything went wrong (for instance the graphics device was lost
                // or reset) we don't have any good way to recover while running on a
                // background thread. Setting the device to null will stop us from
                // rendering, so the main game can deal with the problem later on.
                graphicsDevice = null;
            }
        }

    }
}
